package com.zerro.litez.compiler.processor.file_builder;

import com.squareup.javapoet.*;
import com.zerro.litez.annotations.dao.Delete;
import com.zerro.litez.annotations.dao.Insert;
import com.zerro.litez.annotations.dao.Query;
import com.zerro.litez.annotations.dao.Update;
import com.zerro.litez.compiler.processor.parser.*;

import javax.lang.model.element.*;
import java.sql.SQLException;
import java.util.*;

public class DaoImpBuilder extends FileBuilder<DaoClassParser> {

    String formatter = "formatter";
    String merged = "merged";

    public DaoImpBuilder(DaoClassParser parser, Element e) {
        super(parser, e.getSimpleName() + "_Imp", e);
        onBuilding();
        finish();
    }

    @Override
    protected void onBuilding() {
        getClassBuilder().addSuperinterface(ClassName.get((TypeElement) e))
                .superclass(superClass());
        for (MethodSpec method : buildMethods()) {
            getClassBuilder().addMethod(method);
        }
        getClassBuilder().addMethod(getName());
    }


    private ParameterizedTypeName superClass() {
        return ParameterizedTypeName.get(
                ClassName.get(runtimePackage, "LitEzDao"),
                ClassName.get(parser.getEntityPackage(), parser.getTable()));
    }

    private MethodSpec getName() {
        return MethodSpec.methodBuilder("__$$__getName")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(String.class)
                .addStatement("return $S", e)
                .build();
    }


    protected List<MethodSpec> buildMethods() {
        List<MethodSpec> methodSpecList = new ArrayList<>();

        for (ExecutableElement method : parser.getAnnotatedMethods().keySet()) {
            MethodSpec.Builder builder = MethodSpec.methodBuilder(method.getSimpleName().toString())
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.get(method.getReturnType()));
            addParameters(builder, method);
            addContent(builder, method);
            methodSpecList.add(builder.build());
        }
        return methodSpecList;
    }

    private void addParameters(MethodSpec.Builder builder, ExecutableElement method) {
        for (VariableElement parameter : method.getParameters()) {
            builder.addParameter(TypeName.get(parameter.asType()),
                    parameter.getSimpleName().toString());
        }
    }

    private void addContent(MethodSpec.Builder builder, ExecutableElement method) {
        builder.beginControlFlow("try");
        if (method.getAnnotation(Insert.class) != null) {
            builder.addCode(insertContent(method));
        } else if (method.getAnnotation(Delete.class) != null) {
            builder.addCode(deleteContent(method));

        } else if (method.getAnnotation(Update.class) != null) {
            builder.addCode(updateContent(method));

        } else if (method.getAnnotation(Query.class) != null) {
            builder.addCode(queryContent(method));
        }
        builder.endControlFlow();
        builder.beginControlFlow("catch ($T e)", SQLException.class);
        builder.addStatement("throw new $T(e)", RuntimeException.class);
        builder.endControlFlow();

    }


    private CodeBlock insertContent(ExecutableElement method) {
        List<? extends VariableElement> parameters = method.getParameters();
        StringBuilder columns = new StringBuilder();
        EntityClassParser entityParser = EntityClassParser.getParser(parser.getEntity());
        List<FieldInfo> fieldInfoList = entityParser.getAllInfoOfField(1);
        int size = fieldInfoList.size();
        for (int i = 0; i < size; i++) {
            FieldInfo fieldInfo = fieldInfoList.get(i);
            if (entityParser.getAutoInc() != null && fieldInfo.equals(entityParser.getAutoInc())) {
                continue;
            }
            columns.append(fieldInfo.getColumnName());
            if (i < size - 1) {
                columns.append(", ");
            }
        }
        CodeBlock.Builder code = CodeBlock.builder();
        code.addStatement("String insert = \"insert into $L($L) values\"",
                parser.getTable(), columns);

        for (VariableElement param : parameters) {

            if (param.asType().toString().equals(parser.getEntity())) {
                String doInsert = getDoInsert(param.getSimpleName().toString());
                code.addStatement(doInsert);
            } else {
                code.beginControlFlow("for($L entity: $L)",
                                parser.getTable(), param.getSimpleName())
                        .addStatement(getDoInsert("entity"))
                        .endControlFlow();
            }
        }
        return code.build();
    }

    private String getDoInsert(String name) {


        return "formatter.setGeneratedKeysValue( " + name + ", doInsert(insert, " + name + "))";
    }

    private CodeBlock deleteContent(ExecutableElement method) {
        CodeBlock.Builder code = CodeBlock.builder();
        List<? extends VariableElement> parameters = method.getParameters();
        String en;
        if (parameters.size() == 1) {
            en = convertType(parameters.get(0));
        } else {

            code.add(getMergeBlock(parameters).build());

            en = convertType(merged, false);
        }


        String sqlCode = CodeBlock.builder()
                .add("delete from $L where \" + $L.getPrimaryKeyValues($L)",
                        parser.getTable(), formatter, en).build().toString();

        code.addStatement(execSql(sqlCode));
        return code.build();
    }

    private CodeBlock updateContent(ExecutableElement method) {
        String tableName = "tableName";
        CodeBlock.Builder codeBlock = CodeBlock.builder()
                .addStatement("$T $L = $S", String.class, tableName, parser.getTable());

        List<? extends VariableElement> parameters = method.getParameters();
        String entity;
        if (parameters.size() == 1) {
            entity = parameters.get(0).getSimpleName().toString();
        } else {
            codeBlock.add(getMergeBlock(parameters).build());
            entity = merged;
        }

        codeBlock.addStatement("doUpdate($L, $L)", tableName, entity);

        return codeBlock.build();
    }

    private CodeBlock.Builder getMergeBlock(List<? extends VariableElement> parameters) {
        CodeBlock.Builder mergeBlock;
        mergeBlock = CodeBlock.builder()
                .addStatement("startMergeEntities()");
        for (VariableElement parameter : parameters) {
            String paramType = parameter.asType().toString();
            if (paramType.equals(parser.getEntity()) || isArray(parameter)) {
                mergeBlock.addStatement("addMergeEntity($L)",
                        parameter.getSimpleName().toString());
            } else {
                mergeBlock.addStatement("addMergeEntity( ($L[]) $L.toArray())",
                        parser.getTable(),
                        parameter.getSimpleName().toString());
            }
        }

        mergeBlock.addStatement("$L[] $L = getMergedEntities()", parser.getTable(), merged);
        return mergeBlock;
    }

    private CodeBlock queryContent(ExecutableElement method) {
        List<? extends VariableElement> paramList = method.getParameters();

        String sqlFormat = formatSqlTable(parser.getSqlFormat(method));

        Map<String, Object> params = paramListToMap(paramList);

        if (sqlFormat.contains("$L")) {
            Object[] paramArr = paramListToStringArr(paramList);
            sqlFormat = CodeBlock.builder()
                    .add(sqlFormat, paramArr)
                    .build().toString();
        }
        if (sqlFormat.contains(":L")) {
            sqlFormat = CodeBlock.builder()
                    .addNamed(sqlFormat, params)
                    .build().toString();
        }
        ClassName entityClass = ClassName.get(parser.getEntityPackage(),
                parser.getTable());

        CodeBlock.Builder returnBuilder = CodeBlock.builder();
        String returnType = method.getReturnType().toString();
        String entities = "entities";

        if (!returnType.equals(void.class.getName())) {
            if (returnType.equals(entityClass.toString())) {
                returnBuilder.beginControlFlow("if ($L.length == 0)", entities)
                        .addStatement("return null")
                        .nextControlFlow("else")
                        .addStatement("return $L[0]", entities)
                        .endControlFlow();
            } else if (returnType.equals(entityClass + "[]")) {
                returnBuilder.addStatement("return $L", entities);
            } else if (returnType.contains(Set.class.getName())) {
                returnBuilder.addStatement("return new $T($T.asList($L))",
                        HashSet.class, Arrays.class, entities);
            } else if (returnType.contains(List.class.getName())) {
                returnBuilder.addStatement("return $T.asList($L)", Arrays.class, entities);
            } else {
                returnBuilder.addStatement("return new $T($T.asList($L))",
                        method.getReturnType(), Arrays.class, entities);
            }
        }

        return CodeBlock.builder()
                .addStatement("$T cursor = $L",
                        ClassName.get("java.sql", "ResultSet"),
                        rawQuery(sqlFormat))
                .addStatement("cursor.last()")
                .addStatement("int count = cursor.getRow()")
                .addStatement("$T $L = resultSetToEntityArray(cursor, new $T[count])",
                        ArrayTypeName.of(entityClass), entities, entityClass)
                .add(returnBuilder.build())
                .build();
    }

    private String execSql(String sql) {
        return "supporter.getStmt(\"" + sql + ", false).executeUpdate()";
    }

    private String rawQuery(String sql) {

        return "supporter.getStmt(\"" + sql + "\", false).executeQuery()";
    }

    private String convertType(String param, boolean toArray) {
        if (toArray) {
            return
                    param + ".toArray(new " + parser.getTable() + "[]{})";
        }
        return param;
    }

    private String convertType(VariableElement param) {
        if (!isArray(param)) {
            return "(" + parser.getTable() + "[])" + param.getSimpleName() + ".toArray(new " + parser.getTable() + "[]{})";
        }
        return param.getSimpleName().toString();
    }

    private String formatterValues(VariableElement param) {

        return formatterValues(param.getSimpleName().toString(),
                !isArray(param));
    }

    private boolean isArray(VariableElement param) {
        return (parser.getEntity() + "[]").contains(param.asType().toString());
    }


    private String formatterValues(String param, boolean toArray) {
        String values = formatter + ".values(";
        values += convertType(param, toArray);
        values += ")";

        return values;
    }

    private String formatSqlTable(String sql) {
        return sql.replace("@", parser.getTable());
    }

    private Map<String, Object> paramListToMap(List<? extends VariableElement> paramList) {
        Map<String, Object> map = new LinkedHashMap<>();
        for (int i = 0; i < paramList.size(); i++) {
            String s = paramList.get(i).getSimpleName().toString();
            map.put(s, s);
        }
        return map;
    }

    private Object[] paramListToStringArr(List<? extends VariableElement> paramList) {
        List<String> arr = new ArrayList<>();

        EntityClassParser entityClassParser = EntityClassParser.getParser(parser.getEntity());

        for (VariableElement param : paramList) {
            FieldInfo fieldInfo = entityClassParser.getInfoOfField(param.getSimpleName().toString());
            FieldInfo entityTypePk = fieldInfo.getEntityTypePk();

            CodeBlock.Builder v = CodeBlock.builder();

            if (entityTypePk != null) {
                if (param.asType().toString().contains(entityTypePk.getEntityName())) {
                    if (param.asType().toString().contains("[]")) {
                        v.add("getPrimaryKeys($L, entity -> entity.$L)",
                                        param.getSimpleName(), entityTypePk.getGetter())
                                .build();
                    } else {
                        v.add(param.getSimpleName().toString() + "." + entityTypePk.getGetter());
                    }
                } else {
                    onNormalType(param, v);
                }
            } else {
                onNormalType(param, v);
            }
            if (fieldInfo.isArrayType() && !param.asType().toString().contains("[]")) {
                v = CodeBlock.builder().add("$S + $L + $S", "%\"", v.build(), "\"%");
            }
            arr.add(v.build().toString());
        }
        return arr.toArray(new Object[0]);
    }

    private static void onNormalType(VariableElement param, CodeBlock.Builder v) {
        if (param.asType().toString().contains("[]")) {
            v.add("escape(Arrays.toString($L))", param.getSimpleName()).build().toString();
        } else {
            v.add("escape(" + param.getSimpleName().toString() + ")");
        }
    }


}
